package com.mxgraph.view;

import com.mxgraph.layout.mxIGraphLayout;
import com.mxgraph.model.mxGraphModel;
import com.mxgraph.model.mxGraphModel.mxChildChange;
import com.mxgraph.model.mxGraphModel.mxGeometryChange;
import com.mxgraph.model.mxGraphModel.mxRootChange;
import com.mxgraph.model.mxGraphModel.mxTerminalChange;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.util.*;
import com.mxgraph.util.mxUndoableEdit.mxUndoableChange;

import java.awt.*;
import java.util.*;
import java.util.List;

/**
 * Implements a layout manager that updates the layout for a given transaction.
 * The following example installs an automatic tree layout in a graph:
 * <p/>
 * <code>
 * new mxLayoutManager(graph) {
 * <p/>
 * mxCompactTreeLayout layout = new mxCompactTreeLayout(graph);
 * <p/>
 * public mxIGraphLayout getLayout(Object parent)
 * {
 * if (graph.getModel().getChildCount(parent) > 0) {
 * return layout;
 * }
 * return null;
 * }
 * };
 * </code>
 * <p/>
 * This class fires the following event:
 * <p/>
 * mxEvent.LAYOUT_CELLS fires between begin- and endUpdate after all cells have
 * been layouted in layoutCells. The <code>cells</code> property contains all
 * cells that have been passed to layoutCells.
 */
public class mxLayoutManager extends mxEventSource {

    /**
     * Defines the type of the source or target terminal. The type is a string
     * passed to mxCell.is to check if the rule applies to a cell.
     */
    protected mxGraph graph;

    /**
     * Optional string that specifies the value of the attribute to be passed
     * to mxCell.is to check if the rule applies to a cell. Default is true.
     */
    protected boolean enabled = true;

    /**
     * Optional string that specifies the attributename to be passed to
     * mxCell.is to check if the rule applies to a cell. Default is true.
     */
    protected boolean bubbling = true;

    /**
     *
     */
    protected mxIEventListener undoHandler = new mxIEventListener() {
        public void invoke(Object source, mxEventObject evt) {
            if (isEnabled()) {
                beforeUndo((mxUndoableEdit) evt.getProperty("edit"));
            }
        }
    };

    /**
     *
     */
    protected mxIEventListener moveHandler = new mxIEventListener() {
        public void invoke(Object source, mxEventObject evt) {
            if (isEnabled()) {
                cellsMoved((Object[]) evt.getProperty("cells"), (Point) evt
                        .getProperty("location"));
            }
        }
    };

    /**
     *
     */
    public mxLayoutManager(mxGraph graph) {
        setGraph(graph);
    }

    /**
     * @return the enabled
     */
    public boolean isEnabled() {
        return enabled;
    }

    /**
     * @param value the enabled to set
     */
    public void setEnabled(boolean value) {
        enabled = value;
    }

    /**
     * @return the bubbling
     */
    public boolean isBubbling() {
        return bubbling;
    }

    /**
     * @param value the bubbling to set
     */
    public void setBubbling(boolean value) {
        bubbling = value;
    }

    /**
     * @return the graph
     */
    public mxGraph getGraph() {
        return graph;
    }

    /**
     * @param value the graph to set
     */
    public void setGraph(mxGraph value) {
        if (graph != null) {
            mxIGraphModel model = graph.getModel();
            model.removeListener(undoHandler);
            graph.removeListener(moveHandler);
        }

        graph = value;

        if (graph != null) {
            mxIGraphModel model = graph.getModel();
            model.addListener(mxEvent.BEFORE_UNDO, undoHandler);
            graph.addListener(mxEvent.MOVE_CELLS, moveHandler);
        }
    }

    /**
     *
     */
    protected mxIGraphLayout getLayout(Object parent) {
        return null;
    }

    /**
     *
     */
    protected void cellsMoved(Object[] cells, Point location) {
        if (cells != null && location != null) {
            mxIGraphModel model = getGraph().getModel();

            // Checks if a layout exists to take care of the moving
            for (int i = 0; i < cells.length; i++) {
                mxIGraphLayout layout = getLayout(model.getParent(cells[i]));

                if (layout != null) {
                    layout.moveCell(cells[i], location.x, location.y);
                }
            }
        }
    }

    /**
     *
     */
    protected void beforeUndo(mxUndoableEdit edit) {
        Collection<Object> cells = getCellsForChanges(edit.getChanges());
        mxIGraphModel model = getGraph().getModel();

        if (isBubbling()) {
            Object[] tmp = mxGraphModel.getParents(model, cells.toArray());

            while (tmp.length > 0) {
                cells.addAll(Arrays.asList(tmp));
                tmp = mxGraphModel.getParents(model, tmp);
            }
        }

        layoutCells(mxUtils.sortCells(cells, false).toArray());
    }

    /**
     *
     */
    protected Collection<Object> getCellsForChanges(
            List<mxUndoableChange> changes) {
        Set<Object> result = new HashSet<Object>();
        Iterator<mxUndoableChange> it = changes.iterator();

        while (it.hasNext()) {
            mxUndoableChange change = it.next();

            if (change instanceof mxRootChange) {
                return new HashSet<Object>();
            } else {
                result.addAll(getCellsForChange(change));
            }
        }

        return result;
    }

    /**
     *
     */
    protected Collection<Object> getCellsForChange(mxUndoableChange change) {
        mxIGraphModel model = getGraph().getModel();
        Set<Object> result = new HashSet<Object>();

        if (change instanceof mxChildChange) {
            mxChildChange cc = (mxChildChange) change;
            Object parent = model.getParent(cc.getChild());

            if (cc.getChild() != null) {
                result.add(cc.getChild());
            }

            if (parent != null) {
                result.add(parent);
            }

            if (cc.getPrevious() != null) {
                result.add(cc.getPrevious());
            }
        } else if (change instanceof mxTerminalChange
                || change instanceof mxGeometryChange) {
            Object cell = (change instanceof mxTerminalChange) ? ((mxTerminalChange) change)
                    .getCell()
                    : ((mxGeometryChange) change).getCell();

            if (cell != null) {
                result.add(cell);
                Object parent = model.getParent(cell);

                if (parent != null) {
                    result.add(parent);
                }
            }
        }

        return result;
    }

    /**
     *
     */
    protected void layoutCells(Object[] cells) {
        if (cells.length > 0) {
            // Invokes the layouts while removing duplicates
            mxIGraphModel model = getGraph().getModel();

            model.beginUpdate();
            try {
                for (int i = 0; i < cells.length; i++) {
                    if (cells[i] != model.getRoot()) {
                        executeLayout(getLayout(cells[i]), cells[i]);
                    }
                }

                fireEvent(new mxEventObject(mxEvent.LAYOUT_CELLS, "cells",
                        cells));
            }
            finally {
                model.endUpdate();
            }
        }
    }

    /**
     *
     */
    protected void executeLayout(mxIGraphLayout layout, Object parent) {
        if (layout != null && parent != null) {
            layout.execute(parent);
        }
    }

    /**
     *
     */
    public void destroy() {
        setGraph(null);
    }

}
